/*
 * Copyright 2006-2008 Michele Mostarda ( michele.mostarda@gmail.com ).
 * All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.graphnav.swing;

import org.graphnav.model.Excluded;
import org.graphnav.model.Filter;
import org.graphnav.model.FilteredGraphModel;
import org.graphnav.model.GraphModel;
import org.graphnav.model.ModifiableGraphModel;
import org.graphnav.model.event.GraphModelEvent;
import org.graphnav.model.event.GraphModelListener;

import java.awt.Point;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;


/**
 * Default implementation of {@link EffectProcessor}.
 */
public class DefaultFadeProcessor implements ModifiableGraphModel, EffectProcessor {

    /**
     * Internal sleep worker used to smooth canvas changes.
     */
    private class SlipWorker extends SwingWorker {

        public synchronized Object construct() {
            //Moving the center.
            double xStep = 0, yStep = 0;
            if(centerMoved) {
                xStep = ( newCenter.getX() - panel.getTranslationX()) / D_MOVE_STEPS;
                yStep = ( newCenter.getY() - panel.getTranslationY()) / D_MOVE_STEPS;
            }
            DefaultFadeProcessor.Move move;
            DefaultFadeProcessor.Dissolve dissolve;
            for(int i = 0; i <= MOVE_STEPS; i++) {

                //Move shapes.
                Iterator iter = movements.iterator();
                while(iter.hasNext()) {
                    move = (DefaultFadeProcessor.Move) iter.next();
                    move.performStep(i);
                }

                //Fade shapes.
                if(i <= DISSOLVE_STEPS) {
                   Iterator iter2 = dissolvence.iterator();
                    while(iter2.hasNext()) {
                        dissolve = (DefaultFadeProcessor.Dissolve) iter2.next();
                        dissolve.performStep(i);
                    }
                }

                //Repaint all.
                if(centerMoved) {
                    //FIXME: this should happen by virtually moving the nodes.
                    panel.translateViewLocation(xStep, yStep);
                } else {
                    //FIXME: this should be removed.
                    //panel.repaint();
                    notifyModelChanged(null);
                }

                //Wait.
                try {
                    wait(30);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            return null;
        }

        public void finished() {
            movements.clear();
            dissolvence.clear();
            centerMoved = false;
        }
    }

    private class Move {

        private MovableShape target;

        private Point moveTo;

        private int stepX;
        private int stepY;

        Move(MovableShape ms, Point mt) {
            target = ms;
            moveTo = mt;
            double dx = moveTo.getX() - ms.getBounds2D(panel.getDetailLevel()).getX();
            double dy = moveTo.getY() - ms.getBounds2D(panel.getDetailLevel()).getY();
            stepX = (int) Math.round( dx / D_MOVE_STEPS);
            stepY = (int) Math.round( dy / D_MOVE_STEPS);
        }

        void performStep(int i) {
            if(i < MOVE_STEPS) {
                target.move(stepX, stepY);
            } else {
                target.setLocation((int) moveTo.getX(), (int) moveTo.getY());
            }
        }

    }

    private class Dissolve {

        private Renderable target;

        Dissolve(Renderable r) {
            target = r;
        }

        void performStep(int i) {
            target.setTrasparency((float) i / (float) DISSOLVE_STEPS);
        }
    }

    private class InternalFilter implements Filter {

        private Node selectedNode;

        //private HashSet selectedNodes = new HashSet();

        public void setSelectedNode(Node n) {
            selectedNode = n;
        }

        public void init() {}

        public void release() {
            //selectedNodes.clear();
        }

        public boolean nodeIsVisible(GraphModel model, Node node) {
            if( selectedNode == null || selectedNode.equals(node) || model.adiacent(selectedNode, node) ) {
                //selectedNodes.add(node);
                return true;
            }
            return false;
        }

        public boolean arcIsVisible(GraphModel model, Arc arc) {
            //return selectedNode == null || ( selectedNodes.contains(arc.getFromNode()) && selectedNodes.contains(arc.getToNode()) );
            return true;
        }

        public boolean equals(Object obj) {
            if(obj == null) {
                return false;
            }
            if(obj == this) {
                return true;
            }
            if(obj instanceof InternalFilter) {
                InternalFilter other = (InternalFilter) obj;
                return selectedNode.equals(other.selectedNode);
            }
            return false;
        }
    }

    private static final int MOVE_STEPS = 20;

    //FIXME: now DISSOLVE_STEPS must be < MOVE_STEPS.
    private static final int DISSOLVE_STEPS = 5;

    private static final double D_MOVE_STEPS = (double) MOVE_STEPS;

    private final java.util.List<Move> movements;

    private final java.util.List<Dissolve> dissolvence;

    private final Point newCenter = new Point();

    private boolean centerMoved = false;

    private GraphPanel panel;

    private GraphModel decorated;

    private FilteredGraphModel filteredDecorated;

    private InternalFilter internalFilter;

    private NodeLayout nodeLayout;

    private List<GraphModelListener> listeners;


    protected DefaultFadeProcessor(GraphPanel panel, GraphModel model) {
        this.panel     = panel;

        this.decorated = model;
        this.internalFilter = new InternalFilter();
        this.filteredDecorated = new FilteredGraphModel(decorated, FilteredGraphModel.EMPTY_FILTER);
        this.filteredDecorated.addGraphModelListener(
            new GraphModelListener() {
                public void modelChanged(GraphModelEvent gme) {
                   notifyModelChanged(gme);
                }
            }
        );
        this.nodeLayout = new RoundNodeLayout();

        movements   = new ArrayList<Move>();
        dissolvence = new ArrayList<Dissolve>();
        listeners   = new ArrayList<GraphModelListener>();
    }

    public void shiftView(double sx, double sy) {
        double x = panel.getWidth()  / 2 - sx;
        double y = panel.getHeight() / 2 - sy;
        setNewCenter(x,y);
        go();
    }

    public void enhanceNode(final Node n, boolean showOnlyAdiacents, boolean layoutAdiacents) {
        //Filtering adiacent nodes.
        if(showOnlyAdiacents) {
            internalFilter.setSelectedNode(n);
            filteredDecorated.setFilter(internalFilter); //TODO: fix excluded dissolvence.
            Excluded excluded = filteredDecorated.validate();
            addDissolvence(excluded.excludedNodes());
            addDissolvence(excluded.excludedArcs());
        }

        //Center node and rotating adiacent nodes.
        Rectangle2D nBounds = n.getBounds2D(panel.getDetailLevel());
        double x = panel.getWidth()  / 2 - nBounds.getCenterX();
        double y = panel.getHeight() / 2 - nBounds.getCenterY();
        setNewCenter(x,y);

        if(layoutAdiacents) {
            getNodeLayout().processLayout(this, this, panel.getDetailLevel(), n);
        }

        go();
    }

    public void disableEnhancement() {
       filteredDecorated.setFilter(FilteredGraphModel.EMPTY_FILTER);
    }

    public synchronized EffectProcessor addMovement(MovableShape s, Point p) {
        movements.add( new DefaultFadeProcessor.Move(s, p) );
        return this;
    }

    public synchronized EffectProcessor addDissolvence(Renderable r) {
        dissolvence.add(new DefaultFadeProcessor.Dissolve(r));
        return this;
    }

    public synchronized EffectProcessor addDissolvence(List l) {
        for(Iterator i = l.iterator(); i.hasNext(); ) {
            addDissolvence( (Renderable) i.next() );
        }
        return this;
    }

    public synchronized EffectProcessor setNewCenter(double x, double y) {
        newCenter.setLocation(x,y);
        centerMoved = true;
        return this;
    }

    public EffectProcessor moveToOrigin() {
        setNewCenter(0, 0);
        return this;
    }

    public synchronized void go() {
        DefaultFadeProcessor.SlipWorker sl = new DefaultFadeProcessor.SlipWorker();
        sl.start();
    }

    public void startVisit() {

    }

    public void endVisit() {

    }

    public Iterator<Node> nodes() {
        return filteredDecorated.nodes();
    }

    public Iterator<Arc> arcs() {
        return filteredDecorated.arcs();
    }

    public Node getNode(int i) {
        return filteredDecorated.getNode(i);
    }

    public int nodesCount() {
        return filteredDecorated.nodesCount();
    }

    public Arc getArc(int i) {
        return filteredDecorated.getArc(i);
    }

    public int arcsCount() {
        return filteredDecorated.arcsCount();
    }

    public Iterator<Node> getOutgoingNodes(Node n) {
        return filteredDecorated.getOutgoingNodes(n);
    }

    public Iterator<Node> getIngoingNodes(Node n) {
        return filteredDecorated.getIngoingNodes(n);
    }

    public int outgoingNodesCount(Node n) {
        return filteredDecorated.outgoingNodesCount(n);
    }

    public int ingoingNodesCount(Node n) {
        return filteredDecorated.ingoingNodesCount(n);
    }

    public boolean adiacent(Node n1, Node n2) {
        return filteredDecorated.adiacent(n1, n2);
    }

    public void addGraphModelListener(GraphModelListener gml) {
        listeners.add(gml);
    }

    public void removeGraphModelListener(GraphModelListener gml) {
        listeners.remove(gml);
    }

    public void addNode(Node node) {
       if(decorated instanceof ModifiableGraphModel) {
           ( (ModifiableGraphModel) decorated).addNode(node);
       } else {
           throw new UnsupportedOperationException();
       }
    }

    public void removeNode(Node node) {
       if(decorated instanceof ModifiableGraphModel) {
           ( (ModifiableGraphModel) decorated).removeNode(node);
       } else {
           throw new UnsupportedOperationException();
       }
    }

    public void addArc(Arc a) {
       if(decorated instanceof ModifiableGraphModel) {
           ( (ModifiableGraphModel) decorated).addArc(a);
       } else {
           throw new UnsupportedOperationException();
       }
    }

    public void removeArc(Arc a) {
       if(decorated instanceof ModifiableGraphModel) {
           ( (ModifiableGraphModel) decorated).removeArc(a);
       } else {
           throw new UnsupportedOperationException();
       }
    }

    public Node createNode(Object data, Point pos) {
       if(decorated instanceof ModifiableGraphModel) {
           return ( (ModifiableGraphModel) decorated).createNode(data, pos);
       } else {
           throw new UnsupportedOperationException();
       }
    }

    public Arc createArc(Node src , Node dst, Object data) {
       if(decorated instanceof ModifiableGraphModel) {
           return ( (ModifiableGraphModel) decorated).createArc(src, dst, data);
       } else {
           throw new UnsupportedOperationException();
       }
    }

    protected NodeLayout getNodeLayout() {
        return nodeLayout;
    }

    protected void notifyModelChanged(GraphModelEvent gme) {
        final GraphModelEvent _gme = gme == null ?  new GraphModelEvent(this) : gme;
        Iterator listenersIterator = listeners.iterator();
        while(listenersIterator.hasNext()) {
            ( (GraphModelListener) listenersIterator.next() ).modelChanged(_gme);
        }
    }

}
